﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.Client;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using ThoughtWorks.CruiseControl.Core.Config.Preprocessor;

namespace MSBuild.Tasks
{
	/// <summary>
	/// Create TFS Build definitions based on XML configuration file, with the extra help of the XML preprocessor
	/// </summary>
	/// <example>
	/// <code><![CDATA[
	/// <Buildinator TfsUrl="http://my-tfs:8080/tfs/myprojcoll" TeamProject="MyProj" ConfigFile="build.config" />
	/// ]]></code>
	/// </example>
	public class Buildinator : Task
	{
		private const string PROGRAMNAME = "Buildinator";

		/// <summary>
		/// Gets or sets the URL of the TFS server and Project Collection, e.g. http://g-tfs:8080/tfs/GSTAR
		/// </summary>
		/// <value>The URL.</value>
		[Required]
		public string TfsUrl { get; set; }

		/// <summary>
		/// Gets or sets the Build Uri
		/// </summary>
		/// <value>The Build Uri, as exported by TFS Build when it starts a build.</value>
		[Required]
		public string BuildUri { get; set; }

		/// <summary>
		/// Gets or sets the Build definitions ConfigFile, e.g. build.config
		/// </summary>
		/// <value>The Config File path (relative or absolute).</value>
		[Required]
		public string ConfigFile { get; set; }

		private string teamProject;
		private IBuildServer buildServer;
		private IBuildDetail buildDetail;

		public override bool Execute()
		{
			Log.LogMessage( "{0} started. Calling arguments: {1}", PROGRAMNAME, string.Join( " ", Environment.GetCommandLineArgs() ) );

			var buildDefs = getBuildDefinitions( ConfigFile );

			if( buildDefs.TFSBuild.Length == 0 )
			{
				Log.LogMessage( "No build definitions found" );
				return true;
			}

			Log.LogMessage( "Connecting to {0}", TfsUrl );

			var server = new TfsTeamProjectCollection( new Uri( TfsUrl ) );
			server.EnsureAuthenticated();
			buildServer = (IBuildServer)server.GetService( typeof( IBuildServer ) );
			buildDetail = buildServer.GetBuild( new Uri( BuildUri ) );
			teamProject = buildDetail.TeamProject;

			logStart( "Process", "Updating build definitions" );

			foreach( var build in buildDefs.TFSBuild )
			{
				try
				{
					Log.LogMessage( "Creating {0}", build.Name );
					createBuild( build );
				}
				catch( Exception ex )
				{
					Log.LogErrorFromException( ex, true, true, null );
					logFinish( build.Name );
				}
			}

			logFinish( "Process" );

			return true;
		}

		private readonly HashSet<string> seen = new HashSet<string>();

		private void createBuild( BuildDefsTFSBuild build )
		{
			logStart( build.Name, "Updating " + build.Name );

			// Perform an in-place edit of any existing build definition (By name), or create a new one
			var defs = checkCache( "BuildDefinitions", () => buildServer.QueryBuildDefinitions( teamProject ) );
			var def = defs.FirstOrDefault( d => d.Name.StartsWith( build.Name, StringComparison.InvariantCultureIgnoreCase ) );

			if( def == null )
			{
				Log.LogMessage( "Creating build definition {0}", build.Name );
				def = buildServer.CreateBuildDefinition( teamProject );
			}
			else
			{
				Log.LogMessage( "Modifying current build definition {0}", build.Name );				
			}

			Log.LogMessage( "Setting up basic details for {0}", build.Name );				

			def.Name = build.Name.Trim();
			def.Description = string.Format( "{0}\n\nCreated By: {1}\nCreated: {2}", build.Description, PROGRAMNAME, DateTime.Now );
			/* for VS 2010/TFS 2010:
				def.Enabled = build.EnabledSpecified ? build.Enabled : true;
			*/
			/* for VS2012/TFS 2012: */
			def.QueueStatus = build.EnabledSpecified ? (build.Enabled ? DefinitionQueueStatus.Enabled : DefinitionQueueStatus.Disabled)
   				: DefinitionQueueStatus.Enabled;

			def.ContinuousIntegrationType = (ContinuousIntegrationType)Enum.Parse( typeof( ContinuousIntegrationType ), build.Trigger.Type.ToString() );
			switch( def.ContinuousIntegrationType )
			{
				case ContinuousIntegrationType.Schedule:
					Log.LogMessage( "Defining schedule for {0}", build.Name );				

					if( build.Trigger.Schedule.BuildRegardlessOfChangesSpecified && build.Trigger.Schedule.BuildRegardlessOfChanges )
						def.ContinuousIntegrationType = ContinuousIntegrationType.ScheduleForced;

					var schedule = def.AddSchedule();

					var timezones = checkCache( "TimeZones", TimeZoneInfo.GetSystemTimeZones );
					schedule.TimeZone = timezones.FirstOrDefault( tzi => tzi.DisplayName.Equals( build.Trigger.Schedule.TimeZone, StringComparison.InvariantCultureIgnoreCase ) )
						        ?? TimeZoneInfo.Local;
					schedule.DaysToBuild = buildServer.GetScheduleDaysFromDaysOfWeek( build.Trigger.Schedule.Day.Select( d => (DayOfWeek)Enum.Parse( typeof( DayOfWeek ), d.ToString() ) ).ToArray() );

					DateTime start;
					if( !DateTime.TryParse( build.Trigger.Schedule.StartTime, out start ) )
						start = DateTime.Now.AddSeconds( 30 );
					schedule.StartTime = (int) ( start.TimeOfDay.TotalSeconds );
					break;

				case ContinuousIntegrationType.Batch:
					def.ContinuousIntegrationQuietPeriod = 10;	// minutes
					break;

				case ContinuousIntegrationType.Gated:
					// for VS 2012/TFS 2012: def.BatchSize = 10;	// number of builds to batch
					break;
			}

			def.BuildController = checkCache( build.BuildControllerName, () => buildServer.GetBuildController( build.BuildControllerName + "*" ) );
			def.DefaultDropLocation = build.DropLocation;

			// clear and remake the Workspace Mappings
			Log.LogMessage( "Setting workspace mappings for {0}", build.Name );				

			var mappings = def.Workspace.Mappings.ToArray();
			foreach( var w in mappings )
				def.Workspace.RemoveMapping( w );

			if( build.Map != null )
			{
				foreach( var ws in build.Map )
				{
					var folder = "$(SourceDir)";
					if( !string.IsNullOrWhiteSpace( ws.Folder ) )
						folder = ws.Folder.Replace( "{", "$(" ).Replace( "}", ")" );
					def.Workspace.Map( ws.TFSPath, folder );
				}
			}

			if( build.Cloak != null )
			{
				Log.LogMessage( "Cloaking workspace for {0}", build.Name );
				foreach( var ws in build.Cloak )
				{
					def.Workspace.Cloak( ws.TFSPath );
				}
			}

			// Set the process template
			Log.LogMessage( "Setting the process template for {0}", build.Name );
			var templates = checkCache( build.BuildControllerName, () => buildServer.QueryProcessTemplates( teamProject ) );
			def.Process = templates.First( p => p.ServerPath.ToLower().EndsWith( build.ProcessTemplate.ToLower() ) );

			if( !seen.Contains( def.Process.ServerPath ) )
			{
				Log.LogMessage( "Process Template {0} expects the following parameters: {1}", def.Process.ServerPath, def.Process.Parameters );
				seen.Add( def.Process.ServerPath );
			}

			// Set process parameters
			Log.LogMessage( "Setting the process parameters for {0}", build.Name );
			var parameters = WorkflowHelpers.DeserializeProcessParameters( def.ProcessParameters );

			foreach( var p in build.Parameter )
			{
				Log.LogMessage( "Setting parameter {0}: {1}", p.Key, p.Value );
				if( parameters.ContainsKey( p.Key ) )
					parameters.Remove( p.Key );

				if( p.Key.Equals( "Verbosity", StringComparison.InvariantCultureIgnoreCase ) )
					parameters.Add( "Verbosity", (BuildVerbosity) Enum.Parse( typeof( BuildVerbosity ), p.Value ) );
				else
					parameters.Add( p.Key, p.Value );
			}

			Log.LogMessage( "Defining agent settings" );

			var a = new AgentSettings
			{
				Name = "*",
				TagComparison = TagComparison.MatchExactly,
				MaxExecutionTime = TimeSpan.FromMinutes( 60 ),
				MaxWaitTime = TimeSpan.FromMinutes( 30 )
			};

			if( build.AgentSettings != null )
			{
				if( build.AgentSettings.AgentNameFilter.Length > 0 )
					a.Name = build.AgentSettings.AgentNameFilter;

				if( !string.IsNullOrWhiteSpace( build.AgentSettings.MaxExecutionTime ) )
					a.MaxExecutionTime = TimeSpan.Parse( build.AgentSettings.MaxExecutionTime );

				if( !string.IsNullOrWhiteSpace( build.AgentSettings.MaxReservationTime ) )
					a.MaxWaitTime =  TimeSpan.Parse( build.AgentSettings.MaxReservationTime );

				if( build.AgentSettings.TagsFilterSpecified )
					a.TagComparison = (TagComparison) Enum.Parse( typeof( TagComparison ), build.AgentSettings.TagsFilter.ToString() );

				Log.LogMessage( "Setting agent tags" );

				if( build.AgentSettings.Tags.Length > 0 )
				{
					foreach( var tag in build.AgentSettings.Tags.Split( ";,".ToCharArray(), StringSplitOptions.RemoveEmptyEntries ) )
						a.Tags.Add( tag );
				}
			}

			parameters[ "AgentSettings" ] = a;

			def.ProcessParameters = WorkflowHelpers.SerializeProcessParameters( parameters );

			// Set the Retention Policy
			if( build.RetentionPolicy != null && build.RetentionPolicy.Any() )
			{
				def.RetentionPolicyList.Clear();
				foreach( var p in build.RetentionPolicy )
				{
					var r = (BuildReason) Enum.Parse( typeof( BuildReason ), p.BuildReason.ToString() );
					var s = (BuildStatus) Enum.Parse( typeof( BuildStatus ), p.BuildStatus.ToString() );
					var o = (DeleteOptions) Enum.Parse( typeof( DeleteOptions ), p.DeleteOptions.ToString() );
					def.AddRetentionPolicy( r, s, p.Keep, o );
				}
			}

			// Save the definition
			def.Save();
			Log.LogMessage( "Build definition saved for {0}", def.Name );
			logFinish( build.Name );
		}

		private BuildDefs getBuildDefinitions( string configFilePath )
		{
			Log.LogMessage( "Reading build definitions from {0}", configFilePath );
			var doc = new XmlDocument();
			using( var reader = XmlReader.Create( configFilePath, new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Auto, DtdProcessing = DtdProcessing.Prohibit, IgnoreWhitespace = true } ) )
			{
				using( var writer = doc.CreateNavigator().AppendChild() )
				{
					var preprocessor = new ConfigPreprocessor( new PreprocessorSettings
						{
							IgnoreWhitespace = true,
							NamesAreCaseSensitve = false,
							ExplicitDeclarationRequired = false,
							Namespace = XNamespace.Get( "urn:builddefs.preprocessor" )
						} );
					preprocessor.PreProcess( reader, writer, new XmlUrlResolver(), null );
				}
			}

			var serializer = new System.Xml.Serialization.XmlSerializer( typeof( BuildDefs ) );
			var builds = ( (BuildDefs)( serializer.Deserialize( new XmlNodeReader( doc ) ) ) );
			Log.LogMessage( "Found {0} build definitions", builds.TFSBuild.Length );
			return builds;
		}

		private readonly Dictionary<string, IBuildStep> logTracker = new Dictionary<string, IBuildStep>();
		private void logStart( string name, string message )
		{
			var buildStep = InformationNodeConverters.AddBuildStep( buildDetail, name, message, DateTime.Now, BuildStepStatus.InProgress );
			logTracker[ name ] = buildStep;
		}

		private void logFinish( string name )
		{
			if( logTracker.ContainsKey( name ) )
			{
				var buildStep = logTracker[ name ];
				buildStep.Status = BuildStepStatus.Succeeded;
				buildStep.FinishTime = DateTime.Now;
				buildStep.Save();
			}
		}

		private readonly Dictionary<string, object> objectCache = new Dictionary<string, object>();
		private T checkCache<T>( string key, Func<T> runToGetValue ) where T : class
		{
			T value = null;
			if( objectCache.ContainsKey( key ) )
				value = objectCache[ key ] as T;
			if( value == null )
				objectCache[ key ] = value = runToGetValue();
			return value;
		}
	}
}